// Copyright (c) 2023 Huawei Device Co., Ltd.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::structure::*;
use std::{
    cell::{Ref, RefCell, RefMut},
    rc::Rc,
};

/// `Comment` structure, which corresponding to the `Interface Comment`.
///
/// # Association
/// [Interface Comment](https://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-7D61178C)
///
/// # Example
/// ```
/// use ylong_xml::{Document, DocumentMethod};
///
/// let document = Document::new();
/// let comment = document.create_comment("Comment").unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct Comment {
    pub(crate) inner: Rc<RefCell<CommentInner>>,
}

impl Comment {
    /// Create a `Comment` with the given value and its owner document.
    ///
    /// # Example
    /// ```
    /// use ylong_xml::{Comment, Document, ValuedNodeMethod};
    ///
    /// let document = Document::new();
    /// let comment = Comment::new("Comment", &document).unwrap();
    /// assert_eq!(comment.value(), String::from("Comment"));
    /// ```
    pub fn new(value: &str, owner_document: &Document) -> Result<Self, XmlError> {
        Ok(Self {
            inner: Rc::new(RefCell::new(CommentInner::new(
                value,
                Some(owner_document.downgrade()),
            ))),
        })
    }
}

impl NodeMethod for Comment {
    fn as_node(&self) -> Node {
        Node::Comment(self.clone())
    }

    fn name(&self) -> String {
        String::from("#comment")
    }

    fn node_type(&self) -> NodeType {
        NodeType::Comment
    }

    fn owner_document(&self) -> Option<Document> {
        self.inner.borrow().owner_document.clone()?.upgrade()
    }
}

impl ValuedNodeMethod for Comment {
    fn value(&self) -> String {
        self.inner.borrow().val.clone()
    }

    fn set_value(&mut self, value: &str) {
        self.inner.borrow_mut().val = value.to_string();
    }
}

impl ChildNodeInternalMethod for Comment {
    fn set_parent(&mut self, parent: Option<WeakParentNode>) {
        self.inner.borrow_mut().parent = parent;
    }

    fn is_same_child_node(&self, node: &ChildNode) -> bool {
        node.as_comment().map(|n| n == self).unwrap_or(false)
    }
}

impl ChildNodeMethod for Comment {
    fn as_child_node(&self) -> ChildNode {
        ChildNode::Comment(self.clone())
    }

    fn parent(&self) -> Option<ParentNode> {
        self.inner.borrow().parent.clone()?.upgrade()
    }
}

impl CharacterDataBorrowMethod for Comment {
    type Inner = CommentInner;

    fn borrow(&self) -> Ref<'_, Self::Inner> {
        self.inner.borrow()
    }

    fn borrow_mut(&self) -> RefMut<'_, Self::Inner> {
        self.inner.borrow_mut()
    }
}

impl PartialEq for Comment {
    fn eq(&self, other: &Self) -> bool {
        self.inner.as_ptr() == other.inner.as_ptr()
    }
}

#[derive(Debug)]
pub(crate) struct CommentInner {
    pub(crate) val: String,
    pub(crate) parent: Option<WeakParentNode>,
    pub(crate) owner_document: Option<WeakDocument>,
}

impl CommentInner {
    pub fn new(value: &str, owner_document: Option<WeakDocument>) -> Self {
        Self {
            val: value.to_string(),
            parent: None,
            owner_document,
        }
    }
}

impl CharacterDataInternalMethod for CommentInner {
    fn get_data(&self) -> &String {
        &self.val
    }

    fn get_data_mut(&mut self) -> &mut String {
        &mut self.val
    }
}

#[cfg(test)]
mod ut_comment {
    use crate::structure::*;

    /// UT test case for `Comment::new()`.
    ///
    /// # Brief
    /// 1. Call `new()` method to create a `Comment`.
    /// 2. Check if parts of the `Comment` are default values.
    #[test]
    fn ut_comment_new() {
        let document = Document::new();
        let comment = Comment::new("Comment", &document).unwrap();
        let comment_inner = comment.inner.borrow();
        assert_eq!(comment_inner.val, "Comment");
        assert_eq!(comment_inner.parent, None);
        assert_eq!(comment_inner.owner_document, Some(document.downgrade()));
    }

    /// UT test case for `Comment::as_node()`.
    ///
    /// # Brief
    /// 1. Create a `Comment` called `comment`.
    /// 2. Call `as_node()` method to convert `comment` to `Node`.
    /// 3. Check if `Node::Comment(comment)` is obtained.
    #[test]
    fn ut_comment_as_node() {
        let document = Document::new();
        let comment = document.create_comment("CDATASection").unwrap();
        let node = comment.as_node();
        assert_eq!(node, Node::Comment(comment));
    }

    /// UT test case for `Comment::name()`.
    ///
    /// # Brief
    /// 1. Create a `Comment` called `comment`.
    /// 2. Call `name()` method to obtain `comment`'s name.
    /// 3. Check if the obtained name is the given name.
    #[test]
    fn ut_comment_name() {
        let document = Document::new();
        let comment = document.create_comment("Comment").unwrap();
        assert_eq!(comment.name(), "#comment");
    }

    /// UT test case for `Comment::node_type()`.
    ///
    /// # Brief
    /// 1. Create a `Comment` called `comment`.
    /// 2. Call `node_type()` method to obtain `comment`'s type.
    /// 3. Check if this obtained node type is `NodeType::Comment`.
    #[test]
    fn ut_comment_node_type() {
        let document = Document::new();
        let comment = document.create_comment("Comment").unwrap();
        assert_eq!(comment.node_type(), NodeType::Comment);
    }

    /// UT test case for `Comment::owner_document()`.
    ///
    /// # Brief
    /// 1. Create a `Document` called `document`.
    /// 2. Create an `Comment` of `document`, which is called `comment`.
    /// 3. Call `owner_document()` method to obtain `comment`'s owner document.
    /// 4. Check if `comment`'s owner document is `Some(document)`.
    #[test]
    fn ut_comment_owner_document() {
        let document = Document::new();
        let comment = document.create_comment("Comment").unwrap();
        assert_eq!(comment.owner_document(), Some(document));
    }

    /// UT test case for `Comment::value()`.
    ///
    /// # Brief
    /// 1. Create a `Comment` called `comment` whose value is set to `"Comment"`.
    /// 2. Call `value()` method to obtain `comment`'s value.
    /// 3. Check if the value is equal to `"Comment"`.
    #[test]
    fn ut_comment_value() {
        let document = Document::new();
        let comment = document.create_comment("Comment").unwrap();
        assert_eq!(comment.value(), "Comment");
    }

    /// UT test case for `Comment::set_value()`.
    ///
    /// # Brief
    /// 1. Create a `Comment` called `comment` whose value is set to `"OldComment"`.
    /// 2. Call `set_value()` method to set the value of `comment` to `"NewComment"`.
    /// 3. Get the value of `comment` and check if it is equal to `"NewComment"`.
    #[test]
    fn ut_comment_set_value() {
        let document = Document::new();
        let mut comment = document.create_comment("OldComment").unwrap();
        comment.set_value("NewComment");
        assert_eq!(comment.value(), "NewComment");
    }

    /// UT test case for `Comment::set_parent()`.
    ///
    /// # Brief
    /// 1. Create a `Document` called `document`.
    /// 2. Create a `Comment` called `comment`.
    /// 3. Call `set_parent()` method to set `comment`'s parent to `document`.
    /// 4. Get the parent of `comment` and check if it is the same as `document`.
    #[test]
    fn ut_comment_set_parent() {
        let document = Document::new();
        let mut comment = document.create_comment("Comment").unwrap();
        comment.set_parent(Some(document.as_parent_node().downgrade()));
        assert_eq!(comment.parent(), Some(document.as_parent_node()));
    }

    /// UT test case for `Comment::is_same_child_node()`.
    ///
    /// # Brief
    /// 1. Create two `Comment`s called `comment1` and `comment2`.
    /// 2. Call `as_node()` method to convert these two `Comment`s into
    /// `child1` and `child2` respectively.
    /// 3. Call `is_same_child_node` method to check if `comment1` is the
    /// same as `child1`.
    /// 4. Call `is_same_child_node` method to check if `comment1` is the
    /// same as `child2`.
    #[test]
    fn ut_comment_is_same_child_node() {
        let document = Document::new();
        let comment1 = document.create_comment("Comment").unwrap();
        let child1 = comment1.as_child_node();
        let comment2 = document.create_comment("Comment").unwrap();
        let child2 = comment2.as_child_node();

        assert!(comment1.is_same_child_node(&child1));
        assert!(!comment1.is_same_child_node(&child2));
    }

    /// UT test case for `Comment::as_child_node()`.
    ///
    /// # Brief
    /// 1. Create a `Comment` called `comment`.
    /// 2. Call `as_child_node()` method to convert `comment` to `ChildNode`.
    /// 3. Check if `ChildNode::Comment(comment)` is obtained.
    #[test]
    fn ut_comment_as_child_node() {
        let document = Document::new();
        let comment = document.create_comment("Comment").unwrap();
        let child_node = comment.as_child_node();
        assert_eq!(child_node, ChildNode::Comment(comment));
    }

    /// UT test case for `Comment::parent()`.
    ///
    /// # Brief
    /// 1. Create a `Element` called `element`.
    /// 2. Create a `Comment` called `comment`.
    /// 3. Call `parent()` method to check if `comment` has no parent.
    /// 4. Set `comment`'s parent to `element`.
    /// 5. Call `parent()` method to check if `comment`'s parent is `element`.
    #[test]
    fn ut_comment_parent() {
        let document = Document::new();
        let mut element = document.create_element("Element").unwrap();
        let comment = document.create_comment("Comment").unwrap();
        assert_eq!(comment.parent(), None);

        let _ = element.append_child(comment.clone());
        assert_eq!(comment.parent(), Some(element.as_parent_node()));
    }
}
